<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=549475
-->
<head>
  <title>Test for Bug 549475</title>
  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=549475">Mozilla Bug 549475</a>
<p id="display"></p>
<pre id="test">
<div id='content'>
  <form>
  </form>
</div>
<script type="application/javascript">

SimpleTest.requestLongerTimeout(2);

/**
 * This files tests the 'value sanitization algorithm' for the various input
 * types. Note that an input's value is affected by more than just its type's
 * value sanitization algorithm; e.g. some type=range has actions that the user
 * agent must perform to change the element's value to avoid underflow/overflow
 * and step mismatch (when possible). We specifically avoid triggering these
 * other actions here so that this test only tests the value sanitization
 * algorithm for the various input types.
 *
 * XXXjwatt splitting out testing of the value sanitization algorithm and 
 * "other things" that affect .value makes it harder to know what we're testing
 * and what we've missed, because what's included in the value sanitization
 * algorithm and what's not is different from input type to input type. It
 * seems to me it would be better to have a test (maybe one per type) focused
 * on testing .value for permutations of all other inputs that can affect it.
 * The value sanitization algorithm is just an internal spec concept after all.
 */

// We buffer up the results of sets of sub-tests, and avoid outputting log
// entries for them all if they all pass.  Otherwise, we have an enormous amount
// of test output.

var delayedTests = [];
var anyFailedDelayedTests = false;

function delayed_is(actual, expected, description)
{
  var result = actual == expected;
  delayedTests.push({ actual: actual, expected: expected, description: description });
  if (!result) {
    anyFailedDelayedTests = true;
  }
}

function flushDelayedTests(description)
{
  if (anyFailedDelayedTests) {
    info("Outputting individual results for \"" + description + "\" due to failures in subtests");
    for (var test of delayedTests) {
      is(test.actual, test.expected, test.description);
    }
  } else {
    ok(true, description + " (" + delayedTests.length + " subtests)");
  }
  delayedTests = [];
  anyFailedDelayedTests = false;
}

// We are excluding "file" because it's too different from the other types.
// And it has no sanitizing algorithm.
var inputTypes =
[
  "text", "password", "search", "tel", "hidden", "checkbox", "radio",
  "submit", "image", "reset", "button", "email", "url", "number", "date",
  "time", "range", "color", "month", "week", "datetime-local"
];

var valueModeValue =
[
  "text", "search", "url", "tel", "email", "password", "date", "datetime",
  "month", "week", "time", "datetime-local", "number", "range", "color",
];

function sanitizeDate(aValue)
{
  // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-date-string
  function getNumbersOfDaysInMonth(aMonth, aYear) {
    if (aMonth === 2) {
      return (aYear % 400 === 0 || (aYear % 100 != 0 && aYear % 4 === 0)) ? 29 : 28;
    }
    return (aMonth === 1 || aMonth === 3 || aMonth === 5 || aMonth === 7 ||
            aMonth === 8 || aMonth === 10 || aMonth === 12) ? 31 : 30;
  }

  var match = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})$/.exec(aValue);
  if (!match) {
    return "";
  }
  var year = Number(match[1]);
  if (year === 0) {
    return "";
  }
  var month = Number(match[2]);
  if (month > 12 || month < 1) {
    return "";
  }
  var day = Number(match[3]);
  return 1 <= day && day <= getNumbersOfDaysInMonth(month, year) ? aValue : "";
}

function sanitizeTime(aValue)
{
  // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-time-string
  var match = /^([0-9]{2}):([0-9]{2})(.*)$/.exec(aValue);
  if (!match) {
    return "";
  }
  var hours = match[1];
  if (hours < 0 || hours > 23) {
    return "";
  }
  var minutes = match[2];
  if (minutes < 0 || minutes > 59) {
    return "";
  }
  var other = match[3];
  if (other == "") {
    return aValue;
  }
  match = /^:([0-9]{2})(.*)$/.exec(other);
  if (!match) {
    return "";
  }
  var seconds = match[1];
  if (seconds < 0 || seconds > 59) {
    return "";
  }
  var other = match[2];
  if (other == "") {
    return aValue;
  }
  match = /^.([0-9]{1,3})$/.exec(other);
  if (!match) {
    return "";
  }
  return aValue;
}

function sanitizeDateTimeLocal(aValue)
{
  // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-local-date-and-time-string
  if (aValue.length < 16) {
    return "";
  }

  var separator = aValue[10];
  if (separator != "T" && separator != " ") {
    return "";
  }

  var [date, time] = aValue.split(separator);
  if (!sanitizeDate(date)) {
    return "";
  }

  if (!sanitizeTime(time)) {
    return "";
  }

  // Normalize datetime-local string.
  // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-normalised-local-date-and-time-string
  if (separator == " ") {
    aValue = date + "T" + time;
  }

  if (aValue.length == 16) {
    return aValue;
  }

  if (aValue.length > 19) {
    var milliseconds = aValue.substring(20);
    if (Number(milliseconds) != 0) {
      return aValue;
    }
    aValue = aValue.slice(0, 19);
  }

  var seconds = aValue.substring(17);
  if (Number(seconds) != 0) {
    return aValue;
  }
  aValue = aValue.slice(0, 16);

  return aValue;
}

function sanitizeValue(aType, aValue)
{
  // http://www.whatwg.org/html/#value-sanitization-algorithm
  switch (aType) {
    case "text":
    case "password":
    case "search":
    case "tel":
      return aValue.replace(/[\n\r]/g, "");
    case "url":
    case "email":
      return aValue.replace(/[\n\r]/g, "").replace(/^[\u0020\u0009\t\u000a\u000c\u000d]+|[\u0020\u0009\t\u000a\u000c\u000d]+$/g, "");
    case "number":
      return isNaN(Number(aValue)) ? "" : aValue;
    case "range":
      var defaultMinimum = 0;
      var defaultMaximum = 100;
      var value = Number(aValue);
      if (isNaN(value)) {
        return ((defaultMaximum - defaultMinimum)/2).toString(); // "50"
      }
      if (value < defaultMinimum) {
        return defaultMinimum.toString();
      }
      if (value > defaultMaximum) {
        return defaultMaximum.toString();
      }
      return aValue;
    case "date":
      return sanitizeDate(aValue);
    case "time":
      return sanitizeTime(aValue);
    case "month":
      // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-month-string
      var match = /^([0-9]{4,})-([0-9]{2})$/.exec(aValue);
      if (!match) {
        return "";
      }
      var year = Number(match[1]);
      if (year === 0) {
        return "";
      }
      var month = Number(match[2]);
      if (month > 12 || month < 1) {
        return "";
      }
      return aValue;
    case "week":
      // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-week-string
      function isLeapYear(aYear) {
        return ((aYear % 4 == 0) && (aYear % 100 != 0)) || (aYear % 400 == 0);
      }
      function getDayofWeek(aYear, aMonth, aDay) { /* 0 = Sunday */
        // Tomohiko Sakamoto algorithm.
        var monthTable = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
        aYear -= Number(aMonth < 3);

        return (aYear + parseInt(aYear / 4) - parseInt(aYear / 100) +
                parseInt(aYear / 400) + monthTable[aMonth - 1] + aDay) % 7;
      }
      function getMaximumWeekInYear(aYear) {
        var day = getDayofWeek(aYear, 1, 1);
        return day == 4 || (day == 3 && isLeapYear(aYear)) ? 53 : 52;
      }

      var match = /^([0-9]{4,})-W([0-9]{2})$/.exec(aValue);
      if (!match) {
        return "";
      }
      var year = Number(match[1]);
      if (year === 0) {
        return "";
      }
      var week = Number(match[2]);
      if (week > 53 || month < 1) {
        return "";
      }
      return 1 <= week && week <= getMaximumWeekInYear(year) ? aValue : "";
    case "datetime-local":
      return sanitizeDateTimeLocal(aValue);
    case "color":
      return /^#[0-9A-Fa-f]{6}$/.exec(aValue) ? aValue.toLowerCase() : "#000000";
    default:
      return aValue;
  }
}

function checkSanitizing(element, inputTypeDescription)
{
  var testData =
  [
    // For text, password, search, tel, email:
    "\n\rfoo\n\r",
    "foo\n\rbar",
    "  foo  ",
    "  foo\n\r  bar  ",
    // For url:
    "\r\n foobar    \n\r",
    "\u000B foo \u000B",
    "\u000A foo \u000A",
    "\u000C foo \u000C",
    "\u000d foo \u000d",
    "\u0020 foo \u0020",
    " \u0009 foo \u0009 ",
    // For number and range:
    "42",
    "13.37",
    "1.234567898765432",
    "12foo",
    "1e2",
    "3E42",
    // For date:
    "1970-01-01",
    "1234-12-12",
    "1234567890-01-02",
    "2012-12-31",
    "2012-02-29",
    "2000-02-29",
    "1234",
    "1234-",
    "12345",
    "1234-01",
    "1234-012",
    "1234-01-",
    "12-12",
    "999-01-01",
    "1234-56-78-91",
    "1234-567-78",
    "1234--7-78",
    "abcd-12-12",
    "thisinotadate",
    "2012-13-01",
    "1234-12-42",
    " 2012-13-01",
    " 123-01-01",
    "2012- 3-01",
    "12- 10- 01",
    "  12-0-1",
    "2012-3-001",
    "2012-12-00",
    "2012-12-1r",
    "2012-11-31",
    "2011-02-29",
    "2100-02-29",
    "a2000-01-01",
    "2000a-01-0'",
    "20aa00-01-01",
    "2000a2000-01-01",
    "2000-1-1",
    "2000-1-01",
    "2000-01-1",
    "2000-01-01 ",
    "2000- 01-01",
    "-1970-01-01",
    "0000-00-00",
    "0001-00-00",
    "0000-01-01",
    "1234-12 12",
    "1234 12-12",
    "1234 12 12",
    // For time:
    "1",
    "10",
    "10:",
    "10:1",
    "21:21",
    ":21:21",
    "-21:21",
    " 21:21",
    "21-21",
    "21:21:",
    "21:211",
    "121:211",
    "21:21 ",
    "00:00",
    "-1:00",
    "24:00",
    "00:60",
    "01:01",
    "23:59",
    "99:99",
    "8:30",
    "19:2",
    "19:a2",
    "4c:19",
    "10:.1",
    "1.:10",
    "13:37:42",
    "13:37.42",
    "13:37:42 ",
    "13:37:42.",
    "13:37:61.",
    "13:37:00",
    "13:37:99",
    "13:37:b5",
    "13:37:-1",
    "13:37:.1",
    "13:37:1.",
    "13:37:42.001",
    "13:37:42.001",
    "13:37:42.abc",
    "13:37:42.00c",
    "13:37:42.a23",
    "13:37:42.12e",
    "13:37:42.1e1",
    "13:37:42.e11",
    "13:37:42.1",
    "13:37:42.99",
    "13:37:42.0",
    "13:37:42.00",
    "13:37:42.000",
    "13:37:42.-1",
    "13:37:42.1.1",
    "13:37:42.1,1",
    "13:37:42.",
    "foo12:12",
    "13:37:42.100000000000",
    // For color
    "#00ff00",
    "#000000",
    "red",
    "#0f0",
    "#FFFFAA",
    "FFAABB",
    "fFAaBb",
    "FFAAZZ",
    "ABCDEF",
    "#7654321",
    // For month
    "1970-01",
    "1234-12",
    "123456789-01",
    "2013-13",
    "0000-00",
    "2015-00",
    "0001-01",
    "1-1",
    "888-05",
    "2013-3",
    "2013-may",
    "2000-1a",
    "2013-03-13",
    "december",
    "abcdef",
    "12",
    "  2013-03",
    "2013 - 03",
    "2013 03",
    "2013/03",
    // For week
    "1970-W01",
    "1970-W53",
    "1964-W53",
    "1900-W10",
    "2004-W53",
    "2065-W53",
    "2099-W53",
    "2010-W53",
    "2016-W30",
    "1900-W3",
    "2016-w30",
    "2016-30",
    "16-W30",
    "2016-Week30",
    "2000-100",
    "0000-W01",
    "00-W01",
    "123456-W05",
    "1985-W100",
    "week",
    // For datetime-local
    "1970-01-01T00:00",
    "1970-01-01Z12:00",
    "1970-01-01 00:00:00",
    "1970-01-01T00:00:00.0",
    "1970-01-01T00:00:00.00",
    "1970-01-01T00:00:00.000",
    "1970-01-01 00:00:00.20",
    "1234567-01-01T12:00",
    "2016-13-01T12:00",
    "2016-12-32T12:00",
    "2016-11-08 15:40:30.0",
    "2016-11-08T15:40:30.00",
    "2016-11-07T17:30:10",
    "2016-12-1T12:45",
    "2016-12-01T12:45:30.123456",
    "2016-12-01T24:00",
    "2016-12-01T12:88:30",
    "2016-12-01T12:30:99",
    "2016-12-01T12:30:100",
    "2016-12-01",
    "2016-12-01T",
    "2016-Dec-01T00:00",
    "12-05-2016T00:00",
    "datetime-local"
  ];

  for (value of testData) {
    element.setAttribute('value', value);
    delayed_is(element.value, sanitizeValue(type, value),
       "The value has not been correctly sanitized for type=" + type);
    delayed_is(element.getAttribute('value'), value,
       "The content value should not have been sanitized");

    if (type in valueModeValue) {
      element.setAttribute('value', 'tulip');
      element.value = value;
      delayed_is(element.value, sanitizeValue(type, value),
         "The value has not been correctly sanitized for type=" + type);
      delayed_is(element.getAttribute('value'), 'tulip',
         "The content value should not have been sanitized");
    }

    element.setAttribute('value', '');
    form.reset();
    element.type = 'checkbox'; // We know this type has no sanitizing algorithm.
    element.setAttribute('value', value);
    delayed_is(element.value, value, "The value should not have been sanitized");
    element.type = type;
    delayed_is(element.value, sanitizeValue(type, value),
       "The value has not been correctly sanitized for type=" + type);
    delayed_is(element.getAttribute('value'), value,
       "The content value should not have been sanitized");

    element.setAttribute('value', '');
    form.reset();
    element.setAttribute('value', value);
    form.reset();
    delayed_is(element.value, sanitizeValue(type, value),
       "The value has not been correctly sanitized for type=" + type);
    delayed_is(element.getAttribute('value'), value,
       "The content value should not have been sanitized");

    // Cleaning-up.
    element.setAttribute('value', '');
    form.reset();
  }

  flushDelayedTests(inputTypeDescription);
}

for (type of inputTypes) {
  var form = document.forms[0];
  var element = document.createElement("input");
  element.style.display = "none";
  element.type = type;
  form.appendChild(element);

  checkSanitizing(element, "type=" + type + ", no frame, no editor");

  element.style.display = "";
  checkSanitizing(element, "type=" + type + ", frame, no editor");

  element.focus();
  element.blur();
  checkSanitizing(element, "type=" + type + ", frame, editor");

  element.style.display = "none";
  checkSanitizing(element, "type=" + type + ", no frame, editor");

  form.removeChild(element);
}

</script>
</pre>
</body>
</html>
